/*   The MIT License
 *   
 *   Carina Engine
 *   Copyright (c) 2009 2010 2011 2012 Zdravko Velinov
 *   
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "carina/png-loader.hh"

extern "C"
{
#include <png.h>
}
#include "carina/image.hh"
#include "carina/common/exception.hh"

#include <fstream>

namespace Carina
{
class PNG
{
    png_structp     m_PNGStructPtr;
    png_infop       m_PNGInfoPtr;
    png_infop       m_PNGEndInfoPtr;
    
    std::fstream    m_FS;
public:
    PNG()
        :   m_PNGStructPtr(nullptr),
            m_PNGInfoPtr(nullptr),
            m_PNGEndInfoPtr(nullptr) {}

     ~PNG()
    {
        png_destroy_read_struct(&m_PNGStructPtr, &m_PNGInfoPtr, &m_PNGEndInfoPtr);
    }
            
    ImagePtr load(const string& name)
    {
        const std::streamsize num = 4;
        png_byte header[num];
        m_FS.open(name.c_str(), std::ios::in | std::ios::binary);
        if(!m_FS)
            return ImagePtr();
        m_FS.read(reinterpret_cast<char*>(header), num);
        int not_png = png_sig_cmp(header, 0, num*sizeof(png_byte));
        if(not_png)
            return ImagePtr();
        m_PNGStructPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, &PNG::errorFunction, &PNG::warningFunction);
        if(!m_PNGStructPtr)
            return ImagePtr();
        m_PNGInfoPtr = png_create_info_struct(m_PNGStructPtr);
        if(!m_PNGInfoPtr)
            return ImagePtr();
        m_PNGEndInfoPtr = png_create_info_struct(m_PNGStructPtr);
        if (!m_PNGEndInfoPtr)
            return ImagePtr();

        png_set_read_fn(m_PNGStructPtr, &m_FS, &PNG::readData);
        png_set_sig_bytes(m_PNGStructPtr, num);
        png_uint_32 width, height;
        int     bit_depth, color_type, interlace_type,
                compression_type, filter_method;
        png_read_info(m_PNGStructPtr, m_PNGInfoPtr);
        png_get_IHDR(m_PNGStructPtr, m_PNGInfoPtr, &width, &height,
                     &bit_depth, &color_type, &interlace_type,
                     &compression_type, &filter_method);
        if(bit_depth < 8)
            png_set_expand_gray_1_2_4_to_8(m_PNGStructPtr);
        if (png_get_valid(m_PNGStructPtr, m_PNGInfoPtr, PNG_INFO_tRNS))
            png_set_tRNS_to_alpha(m_PNGStructPtr);
        switch(color_type)
        {
        case PNG_COLOR_TYPE_PALETTE:
            png_set_palette_to_rgb(m_PNGStructPtr); // fall through
        case PNG_COLOR_TYPE_RGB:
            png_set_add_alpha(m_PNGStructPtr, 0xFFFF, PNG_FILLER_AFTER); break;
        }
        
        png_read_update_info(m_PNGStructPtr, m_PNGInfoPtr);
        color_type = png_get_color_type(m_PNGStructPtr, m_PNGInfoPtr);
        bit_depth = png_get_bit_depth(m_PNGStructPtr, m_PNGInfoPtr);
        int rowbytes = png_get_rowbytes(m_PNGStructPtr, m_PNGInfoPtr);
        DynamicArray<char*> p_imgdata(height);
        DynamicArray<char> imgdata(height*rowbytes);
        for(size_t i = 0; i < height; ++i)
            p_imgdata[i] = imgdata.get() + i*rowbytes;
        png_read_image(m_PNGStructPtr, reinterpret_cast<png_bytepp>(p_imgdata.get()));
        ImagePtr imgptr;
        switch(color_type)
        {
        case PNG_COLOR_TYPE_GRAY:
        {
            switch(bit_depth)
            {
            case 8: imgptr = make_aligned_shared<Image>(VT_R8_UNORM, imgdata, width, height); break;
            case 16: imgptr = make_aligned_shared<Image>(VT_R16_UNORM, imgdata, width, height); break;
            }
        } break;
        case PNG_COLOR_TYPE_GRAY_ALPHA:
        {
            switch(bit_depth)
            {
            case 8: imgptr = make_aligned_shared<Image>(VT_RG8_UNORM, imgdata, width, height); break;
            case 16: imgptr = make_aligned_shared<Image>(VT_RG16_UNORM, imgdata, width, height); break;
            }
        } break;
        case PNG_COLOR_TYPE_RGB_ALPHA:
        {
            switch(bit_depth)
            {
            case 8: imgptr = make_aligned_shared<Image>(VT_RGBA8_UNORM, imgdata, width, height); break;
            case 16: imgptr = make_aligned_shared<Image>(VT_RGBA16_UNORM, imgdata, width, height); break;
            }
        } break;
        default:
            return ImagePtr();
        }

        png_read_end(m_PNGStructPtr, m_PNGEndInfoPtr);
        return imgptr;
    }
    
    bool save(const string& name, const ImagePtr& img)
    {
        return false;
    }
    
private:
    static void errorFunction(png_structp png_ptr, png_const_charp error_msg)
    {
        THROW_EXCEPTION(string("a fatal error has occurred while trying to load an png file: ") + error_msg);
    }
    
    static void warningFunction(png_structp png_ptr, png_const_charp warning_msg)
    {
        Log::message(CE_LOG_WARNING, string("PNG: ") + warning_msg);
    }
    
    static void readData(png_structp png_ptr, png_bytep data, png_size_t length)
    {
        if(png_ptr == nullptr)
            return;
        std::fstream* fs = reinterpret_cast<std::fstream*>(png_get_io_ptr(png_ptr)); assert(fs);
        fs->read(reinterpret_cast<char*>(data), static_cast<std::streamsize>(length));
        if(!fs)
            png_error(png_ptr, "Read Error");
    }
};

ImagePtr PNGLoader::load(const string& name)
{
    PNG png;
    return png.load(name);
}

bool PNGLoader::save(const string& name, const ImagePtr& img)
{
    PNG png;
    return png.save(name, img);
}
}
